package ubo.master1.jeupoker.ia;

import ubo.master1.jeupoker.gui.LaCart;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * TablePoker de poker.
 * 
 */
public class TablePoker {
    
    /** Maximum de mise bet ou suite raise pour une seule mains par tour. */
    private static final int MAX_RAISES = 4;
    
    /** La mise du bigBlind. */
    private final int bigBlind;
    
    /** Les joueurs de la table. */
    private final List<Joueur> players;
    
    /** Les joueurs active dans le jeu couran. */
    private final List<Joueur> activePlayers;
    
    /** Jeu de carte. */
    private final GenereCarte deck;
    
    /** les carte sur la table. */
    private final List<LaCart> board;
    
    /** Position courante du dealer. */
    private int dealerPosition;

    /** Le dealer actuel. */
    private Joueur dealer;

    /** Position du joueur ayant la main de jouer. */
    private int actorPosition;
    
    /** Le joueur actuel. */
    private Joueur actor;

    /** La mise minumum dans le round courant. */
    private int minBet;
    
    /** La mise actuel dans le round courant. */
    private int bet;
    
    /** Le pot . */
    private int pot;
    
    /** Lorsque le jeu est terminé. */
    private boolean gameOver;
    
    /**
     * Constructeur.
     * 
     * @param bigBlind
     *            La somme du big Blind.
     */
    public TablePoker(int bigBlind) {
        this.bigBlind = bigBlind;
        players = new ArrayList<Joueur>();
        activePlayers = new ArrayList<Joueur>();
        deck = new GenereCarte();
        board = new ArrayList<LaCart>();
    }
    
    /**
     * Ajout d'un joueur.
     * 
     * @param player
     *            Le joueur.
     */
    public void addPlayer(Joueur player) {
        players.add(player);
    }
    
    /**
     * Lancement du jeu.
     */
    public void start() {
        resetGame();
        while (!gameOver) {
            playHand();
        }
        notifyMessage("Game over.");
    }
    
    /**
     * Réinitialiser le jeu.
     */
    private void resetGame() {
        dealerPosition = -1;
        actorPosition = -1;
        gameOver = false;
        for (Joueur player : players) {
            player.getClient().joinedTable(bigBlind, players);
        }
    }
    
    /**
     * Le jeu d'une main.
     */
    private void playHand() {
        resetHand();
        
        // La petit mise small blind.
        rotateActor();
        postSmallBlind();
        
        // La grande mise big blind.
        rotateActor();
        postBigBlind();
        
        // Pre-Flop.
        dealHoleCards();
        doBettingRound();
        
        // Flop. affichage des trois première cartes.
        if (activePlayers.size() > 1) {
            bet = 0;
            dealCommunityCards("Flop", 3);
            doBettingRound();

            // Turn. affichge de la 4ème carte.
            if (activePlayers.size() > 1) {
                bet = 0;
                dealCommunityCards("Turn", 1);
                minBet = 2 * bigBlind;
                doBettingRound();

                // River. affichage de la 5ème carte.
                if (activePlayers.size() > 1) {
                    bet = 0;
                    dealCommunityCards("River", 1);
                    doBettingRound();

                    // Showdown.
                    if (activePlayers.size() > 1) {
                        bet = 0;
                        doShowdown();
                    }
                }
            }
        }
    }
    
    /**
     * Réeinitialiser la main d'un joueur.
     */
    private void resetHand() {
        board.clear();
        bet = 0;
        pot = 0;
        notifyBoardUpdated();
        activePlayers.clear();
        for (Joueur player : players) {
            player.resetHand();
            if (!player.isBroke()) {
                activePlayers.add(player);
            }
        }
        dealerPosition = (dealerPosition + 1) % players.size();
        dealer = players.get(dealerPosition);
        deck.shuffle();
        actorPosition = dealerPosition;
        minBet = bigBlind;
        bet = minBet;
        for (Joueur player : players) {
            player.getClient().handStarted(dealer);
        }
        notifyPlayersUpdated(false);
        notifyMessage("Nouvelle main, %s est le dealer.", dealer);
    }

    /**
     * Avancer la postion du joueur.
     */
    private void rotateActor() {
        if (activePlayers.size() > 0) {
            do {
                actorPosition = (actorPosition + 1) % players.size();
                actor = players.get(actorPosition);
            } while (!activePlayers.contains(actor));
            for (Joueur player : players) {
                player.getClient().actorRotated(actor);
            }
        } else {

            throw new IllegalStateException("No active players left");
        }
    }
    
    /**
     * La petit mise.
     */
    private void postSmallBlind() {
        final int smallBlind = bigBlind / 2;
        actor.postSmallBlind(smallBlind);
        pot += smallBlind;
        notifyBoardUpdated();
        notifyPlayerActed();
    }
    
    /**
     * La grande mise.
     */
    private void postBigBlind() {
        actor.postBigBlind(bigBlind);
        pot += bigBlind;
        notifyBoardUpdated();
        notifyPlayerActed();
    }
    
    /**
     * Afficher les deux carte des joueurs.
     */
    private void dealHoleCards() {
        for (Joueur player : players) {
            player.setCards(deck.deal(2));
        }
        notifyPlayersUpdated(false);
        notifyMessage("%s consulte ses cartes.", dealer);
    }
    
    /**
     * Affichage des cartes caché du tapi.
     * 
     * @param phaseName
     *            Le nom de la phase.
     * @param noOfCards
     *            Le nombre de carte à afficher.
     */
    private void dealCommunityCards(String phaseName, int noOfCards) {
        for (int i = 0; i < noOfCards; i++) {
            board.add(deck.deal());
        }
        notifyPlayersUpdated(false);
        notifyMessage("%s deals the %s.", dealer, phaseName);
    }
    
    /**
     * Effectuer une mise sur un tour de jeu.
     */
    private void doBettingRound() {
        // Détérminser le nombre de joueurs actives.
        int playersToAct = activePlayers.size();
        // Détérminer le joueur initial et la petit mise
        if (board.size() == 0) {
            // Pre-Flop; mise automatique du jouer a gauche du big blind (Grand mise).
            bet = bigBlind;
        } else {
            // Sinon, le joueur de gauche du dealer commence à jouer.
            actorPosition = dealerPosition;
            bet = 0;
        }
        notifyBoardUpdated();
        while (playersToAct > 0) {
            rotateActor();
            Set<LesActions> allowedActions = getAllowedActions(actor);
            LesActions action = actor.act(allowedActions, minBet, bet);
            if (!allowedActions.contains(action)) {
                String msg = String.format("Action illegal (%s) du joueur %s!", action, actor);
                throw new IllegalStateException(msg);
            }
            playersToAct--;
            switch (action) {
                case CHECK:
                    // Rien à faire.
                    break;
                case CALL:
                    pot += actor.getBetIncrement();
                    break;
                case BET:
                    bet = minBet;
                    pot += actor.getBetIncrement();
                    playersToAct = activePlayers.size();
                    break;
                case RAISE:
                    bet += minBet;
                    pot += actor.getBetIncrement();
                    if (actor.getRaises() == MAX_RAISES) {
                        // Max : maximum de suivi autorisé 
                        playersToAct = activePlayers.size() - 1;
                    } else {
                        // sinon, chacun des autre joueur peuvent suivre.
                        playersToAct = activePlayers.size();
                    }
                    break;
                case FOLD:
                    actor.setCards(null);
                    activePlayers.remove(actor);
                    if (activePlayers.size() == 1) {
                        // Le joueur a gauche gagne
                        playerWins(activePlayers.get(0));
                        playersToAct = 0;
                    }
                    break;
                default:
                    throw new IllegalStateException("Invalid action: " + action);
            }
            if (actor.isBroke()) {
                actor.setInAllPot(pot);
            }
            notifyBoardUpdated();
            notifyPlayerActed();
        }
        
        // Réenitialiser les mises des joueurs.
        for (Joueur player : activePlayers) {
            player.resetBet();
        }
        notifyBoardUpdated();
        notifyPlayersUpdated(false);
    }

    /**
     * Renvoyer les action autorisé pour un joueur donnée.
     * 
     * @param player
     *            Le joueur.
     * 
     * @return Les actioins autorisés.
     */
    private Set<LesActions> getAllowedActions(Joueur player) {
        int actorBet = actor.getBet();
        Set<LesActions> actions = new HashSet<LesActions>();
        if (bet == 0) {
            actions.add(LesActions.CHECK);
            if (player.getRaises() < MAX_RAISES) {
                actions.add(LesActions.BET);
            }
        } else {
            if (actorBet < bet) {
                actions.add(LesActions.CALL);
                if (player.getRaises() < MAX_RAISES) {
                    actions.add(LesActions.RAISE);
                }
            } else {
                actions.add(LesActions.CHECK);
                if (player.getRaises() < MAX_RAISES) {
                    actions.add(LesActions.RAISE);
                }
            }
        }
        actions.add(LesActions.FOLD);
        return actions;
    }
    
    /**
     * Performs the Showdown.
     */
    private void doShowdown() {
//	System.out.print("Board: ");
//	for (LaCart card : board) {
//	    System.out.print(card + " ");
//	}
//	System.out.println();
//        // Look at each hand value, sorted from highest to lowest.
        Map<ValeurMain, List<Joueur>> rankedPlayers = getRankedPlayers();
        for (ValeurMain handValue : rankedPlayers.keySet()) {
            // retourner le joueur gagnant la partie
            List<Joueur> winners = rankedPlayers.get(handValue);
            if (winners.size() == 1) {
                // Un seul gangnant.
                Joueur winner = winners.get(0);
                winner.win(pot);
                notifyBoardUpdated();
                notifyPlayersUpdated(true);
                notifyMessage("%s gagne le pot.", winner);
                break;
            } else {
                // Tie; plusieur gagnats partager le pot.
                int tempPot = pot;
                StringBuilder sb = new StringBuilder("Tie: ");
                for (Joueur player : winners) {
                    // Détérminer les joueurs qui partage le pot.
                    int potShare = player.getAllInPot();
                    if (potShare == 0) {
                        potShare = pot / winners.size();
                    }
                    // Donner au gagant le somme du pot a partager.
                    player.win(potShare);
                    tempPot -= potShare;
                    if (sb.length() > 0) {
                        sb.append(", ");
                    }
                    sb.append(String.format("%s gagne $%d\n", player, potShare));
                    // Si pas de pot a partager terminer 
                    if (tempPot == 0) {
                        break;
                    }
                }
                notifyBoardUpdated();
                notifyPlayersUpdated(true);
                notifyMessage(sb.append('.').toString());
                if (tempPot > 0) {
                    throw new IllegalStateException("Le pot n'est pas null après partage avec les gagnant");
                }
                break;
            }
        }
    }
    
    /**
     * Retourner le joueurs le mieu noté .
     * 
     * Les jeours sont sauvegarder par ordre decroissant.
     * 
     * @return Le joueur active. 
     */
    private Map<ValeurMain, List<Joueur>> getRankedPlayers() {
	Map<ValeurMain, List<Joueur>> winners = new TreeMap<ValeurMain, List<Joueur>>();
	for (Joueur player : activePlayers) {
            UneMain hand = new UneMain(board);
            hand.addCards(player.getCards());
            ValeurMain handValue = new ValeurMain(hand);
            List<Joueur> playerList = winners.get(handValue);
            if (playerList == null) {
        	playerList = new LinkedList<Joueur>();
            }
            playerList.add(player);
            winners.put(handValue, playerList);
	}
	return winners;
    }
    
    /**
     * Le joueur gagne le pot.
     * 
     * @param player
     *            Le joueur gagnant.
     */
    private void playerWins(Joueur player) {
        player.win(pot);
        pot = 0;
        notifyBoardUpdated();
        notifyMessage("%s Gagne.", player);
    }
    
    /**
     * Notifies listeners with a custom game message.
     * 
     * @param message
     *            The formatted message.
     * @param args
     *            Any arguments.
     */
    private void notifyMessage(String message, Object... args) {
        message = String.format(message, args);
        for (Joueur player : players) {
            player.getClient().messageReceived(message);
        }
    }
    
    /**
     * Notification du client que la table a été mise à jour.
     */
    private void notifyBoardUpdated() {
        for (Joueur player : players) {
            player.getClient().boardUpdated(board, bet, pot);
        }
    }

    /**
     * Notification du client qu'un ou plusieurs joueur ont été mis à jour.
     */
    private void notifyPlayersUpdated(boolean showdown) {
        for (Joueur playerToNotify : players) {
            for (Joueur player : players) {
                if (!showdown && !player.equals(playerToNotify)) {
                    // Hide secret information to other players.
                    player = player.publicClone();
                }
                playerToNotify.getClient().playerUpdated(player);
            }
        }
    }
    
    /**
     * notification du client de l'action du joueur.
     */
    private void notifyPlayerActed() {
        for (Joueur p : players) {
            Joueur playerInfo = p.equals(actor) ? actor : actor.publicClone();
            p.getClient().playerActed(playerInfo);
        }
    }
    
}
